/*  Copyright (c) 2012 William Rogers and James Boud

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

using System;
using System.ComponentModel;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using JBBRXG11ContentTexture;

using TInput = Microsoft.Xna.Framework.Content.Pipeline.Graphics.NodeContent;
using TOutput = JBBRXG11ContentFBXModel.ModelInfoStuff;

namespace JBBRXG11ContentFBXModel
{
    // First a local override of the material processor used by this content processor.
    // This enables us to redirect the texture processing to the JBBRXG11 version.
    // Without this, the standard FBX content processor connects to the XNA texture
    // processor (without regard to the default texture processor setup)
    [ContentProcessor(DisplayName = "JBBRXG11 Model Material Processor")]
    public class TameMaterialProcessor : MaterialProcessor
    {
        protected override ExternalReference<TextureContent> BuildTexture
            (string textureName, ExternalReference<TextureContent> texture, ContentProcessorContext context)
        {
            ExternalReference<Texture2DInfoStuff> tisref;
            OpaqueDataDictionary texparams = new OpaqueDataDictionary();
            texparams.Add("ColorKeyEnabled", false);
            texparams.Add("GenerateMipmaps", true);
            texparams.Add("ResizeToPowerOfTwo", false);
            tisref = context.BuildAsset<TextureContent, JBBRXG11ContentTexture.Texture2DInfoStuff>
                             (texture, "JBBRXG11Texture2DContentProcessor",
                             texparams, "JBBRXG11Texture2DContentImporter", "");
            return new ExternalReference<TextureContent>(tisref.Filename);  // Lie about the result
        }
    }

    // Now the FBX model content processor itself
    [ContentProcessor(DisplayName = "JBBRXG11 FBX Model Processor")]
    public class JBBRXG11FBXModelContentProcessor : ContentProcessor<TInput, TOutput>
    {
        // TODO:  Could register and pass through texture parameters for material processing

        #region Processor Parameters
        // For the processor attributes to work need "using System.ComponentModel"
        bool computeadjacencyindices = false;

        [DisplayName("Compute adjacency indices")]
        [DefaultValue("False")]
        public bool ComputeAdjacencyIndices { get { return computeadjacencyindices; } set { computeadjacencyindices = value; } }
        #endregion Processor Parameters

        // Cannot inherit from ModelProcessor because a different result type
        // is needed.  However it's effort is required, but with a modified
        // material processor - so it is subclassed here and used later
        private class TameModelProcessor : ModelProcessor
        {
            protected override MaterialContent ConvertMaterial(MaterialContent material,
                                                               ContentProcessorContext context)
            {
                return context.Convert<MaterialContent, MaterialContent>(material, "TameMaterialProcessor");
            }
        }

        const int MaxBones = 59;
        string dbtxt = "";

        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            my_mesh[] meshes;
            SkinningData skinning;

            BoneContent skeleton = MeshHelper.FindSkeleton(input);
            if (skeleton == null)                                             // No skeleton
            {
                //throw new InvalidContentException("Input skeleton not found.");
                context.Logger.LogWarning(null, null, "Mesh has no skeleton.");
                skinning = null;
            }
            else
            {                                                                 // Has skeleton, maybe animations
                ValidateMesh(input, context, null);

                // We don't want to have to worry about different parts of the model being
                // in different local coordinate systems, so let's just bake everything.
                FlattenTransforms(input, skeleton);

                // Read the bind pose and skeleton hierarchy data.
                IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);
                if (bones.Count > MaxBones)
                {
                    throw new InvalidContentException(string.Format(
                        "Skeleton has {0} bones, but the maximum supported is {1}.",
                        bones.Count, MaxBones));
                }

                // Package the bone information and the animation data
                skinning = ProcessSkeleton(bones, skeleton.Animations, context);
            }

            // Use the base class processor to do whatever it does to the mesh
            // data, then package it into our data format for writing.
            ModelProcessor mp = new TameModelProcessor();
            ModelContent content = mp.Process(input, context);
            meshes = ProcessMeshData(content, context.OutputDirectory);

            return new TOutput(meshes, skinning, dbtxt);
        }

        /// <summary>
        /// Makes sure this mesh contains the kind of data we know how to animate.
        /// </summary>
        static void ValidateMesh(NodeContent node, ContentProcessorContext context,
                                 string parentBoneName)
        {
            MeshContent mesh = node as MeshContent;
            if (mesh != null)
            {
                // Validate the mesh.
                if (parentBoneName != null)
                {
                    context.Logger.LogWarning(null, null,
                        "Mesh {0} is a child of bone {1}. SkinnedModelProcessor " +
                        "does not correctly handle meshes that are children of bones.",
                        mesh.Name, parentBoneName);
                }

                if (!MeshHasSkinning(mesh))
                {
                    context.Logger.LogWarning(null, null,
                        "Mesh {0} has no skinning information, so it has been deleted.",
                        mesh.Name);

                    mesh.Parent.Children.Remove(mesh);
                    return;
                }
            }
            else if (node is BoneContent)
            {
                // If this is a bone, remember that we are now looking inside it.
                parentBoneName = node.Name;
            }

            // Recurse (iterating over a copy of the child collection,
            // because validating children may delete some of them).
            foreach (NodeContent child in new List<NodeContent>(node.Children))
                ValidateMesh(child, context, parentBoneName);
        }

        /// <summary>
        /// Checks whether a mesh contains skininng information.
        /// </summary>
        static bool MeshHasSkinning(MeshContent mesh)
        {
            foreach (GeometryContent geometry in mesh.Geometry)
            {
                if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights()))
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Bakes unwanted transforms into the model geometry,
        /// so everything ends up in the same coordinate system.
        /// </summary>
        static void FlattenTransforms(NodeContent node, BoneContent skeleton)
        {
            foreach (NodeContent child in node.Children)
            {
                // Don't process the skeleton, because that is special.
                if (child == skeleton)
                    continue;

                // Bake the local transform into the actual geometry.
                MeshHelper.TransformScene(child, child.Transform);

                // Having baked it, we can now set the local
                // coordinate system back to identity.
                child.Transform = Matrix.Identity;

                // Recurse.
                FlattenTransforms(child, skeleton);
            }
        }

        my_mesh[] ProcessMeshData(ModelContent cont, string outputdir)
        {
            // Copy ModelContent data into a data structure that will write/read automatically
            my_mesh[] meshes;
            int m = 0;

            dbtxt += cont.Meshes.Count.ToString() + " meshes: ";
            meshes = new my_mesh[cont.Meshes.Count];
            foreach (ModelMeshContent mesh in cont.Meshes)
            {
                meshes[m].name = mesh.Name;                                      // Name
                meshes[m].sphere = mesh.BoundingSphere;
                dbtxt += " " + mesh.Name + " " + mesh.MeshParts.Count.ToString() + "[";

                meshes[m].parts = new my_mesh_part[mesh.MeshParts.Count];        // Mesh parts
                int p = 0;
                foreach (ModelMeshPartContent part in mesh.MeshParts)
                {
                    // Decide on and set appropriate index buffer data
                    int maxindex;
                    int ibuffindex;
                    bool outoforderindex, noindices, use32bitindices;
                    dbtxt += "IB count = " + part.IndexBuffer.Count.ToString();

                    maxindex = 0;
                    outoforderindex = false;
                    ibuffindex = 0;
                    foreach (int ibuffint in part.IndexBuffer)
                    {
                        if (ibuffint != ibuffindex) outoforderindex = true;
                        if (ibuffint > maxindex) maxindex = ibuffint;
                        ibuffindex++;
                    }
                    if (!computeadjacencyindices && !outoforderindex)
                    {
                        noindices = true;
                        use32bitindices = false;
                        dbtxt += " No index buffer";
                        meshes[m].parts[p].p16Indices = null;
                        meshes[m].parts[p].p32Indices = null;
                    }
                    else if (maxindex <= ushort.MaxValue)
                    {
                        noindices = false;
                        use32bitindices = false;
                        dbtxt += " 16 bit index buffer";
                        meshes[m].parts[p].p16Indices = new ushort[part.IndexBuffer.Count];
                        ibuffindex = 0;
                        foreach (int ibuffint in part.IndexBuffer)
                        {
                            meshes[m].parts[p].p16Indices[ibuffindex] = (ushort)ibuffint;
                            ibuffindex++;
                        }
                        meshes[m].parts[p].p32Indices = null;
                    }
                    else
                    {
                        noindices = false;
                        use32bitindices = true;
                        meshes[m].parts[p].p16Indices = null;
                        dbtxt += "32 bit index buffer";
                        meshes[m].parts[p].p32Indices = new int[part.IndexBuffer.Count];
                        ibuffindex = 0;
                        foreach (int ibuffint in part.IndexBuffer)
                        {
                            meshes[m].parts[p].p32Indices[ibuffindex] = ibuffint;
                            ibuffindex++;
                        }
                    }
                    
                    meshes[m].parts[p].computeadjacencyindices = computeadjacencyindices;
                    meshes[m].parts[p].noindices = noindices;
                    meshes[m].parts[p].use32bitindices = use32bitindices;

                    // Selection parameters
                    meshes[m].parts[p].numvertices = part.NumVertices;
                    meshes[m].parts[p].primitivecount = part.PrimitiveCount;
                    meshes[m].parts[p].startindex = part.StartIndex;
                    meshes[m].parts[p].vertexOffset = part.VertexOffset;
                    dbtxt += " , #V = " + part.NumVertices.ToString() + (computeadjacencyindices ? ", computeadj" : "")
                        + (noindices ? ", noindices" : "") + (use32bitindices ? ", use32bit" : "");

                    // The vertex buffer
                    meshes[m].parts[p].pVertices = part.VertexBuffer.VertexData;
                    dbtxt += ", VB count = " + (part.VertexBuffer.VertexData.Length/part.VertexBuffer.VertexDeclaration.VertexStride).ToString();

                    // Vertex declaration - save vertex element array in info data structure
                    meshes[m].parts[p].vertexelements = new my_vertex_element[part.VertexBuffer.VertexDeclaration.VertexElements.Count];
                    int v = 0;
                    foreach (VertexElement x in part.VertexBuffer.VertexDeclaration.VertexElements)
                    {
                        meshes[m].parts[p].vertexelements[v].ve = x;
                        v++;
                    }
                    
                    //VertexElement[] velems = new VertexElement[part.VertexBuffer.VertexDeclaration.VertexElements.Count];
                    //foreach (VertexElement x in part.VertexBuffer.VertexDeclaration.VertexElements)
                    //{
                    //    velems[tempi] = x;
                    //    tempi++;
                    //}
                    //// = part.VertexBuffer.VertexDeclaration.VertexElements;           // Vertex declaration built at runtime
                    //meshes[m].parts[p].vertexelements = new my_vertex_element[velems.Length];
                    //for (int v = 0; v < velems.Length; v++)
                    //{
                    //    meshes[m].parts[p].vertexelements[v].ve = velems[v];
                    //}

                    // Materials - only textures are implemented
                    int numtex = part.Material.Textures.Count;
                    meshes[m].parts[p].texturekeys = new string[numtex];
                    meshes[m].parts[p].texturefilenames = new string[numtex];
                    int t = 0;
                    foreach (string key in part.Material.Textures.Keys)
                    {
                        meshes[m].parts[p].texturekeys[t] = key;
                        string fname = part.Material.Textures[key].Filename;
                        if (fname.IndexOf(outputdir) == 0)
                            fname = fname.Substring(outputdir.Length);
                        meshes[m].parts[p].texturefilenames[t] = fname;
                        t++;
                    }
                    p++;
                    dbtxt += "]";
                }
                m++;
            }
            return meshes;
        }

        SkinningData ProcessSkeleton(IList<BoneContent> bones,
                                     AnimationContentDictionary animations,
                                     ContentProcessorContext context)
        {
            Matrix[] bindPose = new Matrix[bones.Count];
            Matrix[] inverseBindPose = new Matrix[bones.Count];
            int[] skeletonHierarchy = new int[bones.Count];
            int b;

            b = 0;
            foreach (BoneContent bone in bones)
            {
                bindPose[b] = bone.Transform;
                inverseBindPose[b] = Matrix.Invert(bone.AbsoluteTransform);
                skeletonHierarchy[b] = bones.IndexOf(bone.Parent as BoneContent);
                b++;
            }

            // Convert animation data to our runtime format.
            Dictionary<string, AnimationClip> animationClips;
            animationClips = ProcessAnimations(animations, bones, context);

            return new SkinningData(animationClips, bindPose, inverseBindPose, skeletonHierarchy);
        }

        /// <summary>
        /// Converts an intermediate format content pipeline AnimationContentDictionary
        /// object to our runtime AnimationClip format.
        /// </summary>
        static Dictionary<string, AnimationClip> ProcessAnimations(AnimationContentDictionary animations,
                                                                   IList<BoneContent> bones,
                                                                   ContentProcessorContext context)
        {
            // Build up a table mapping bone names to indices.
            Dictionary<string, int> boneMap = new Dictionary<string, int>();

            for (int i = 0; i < bones.Count; i++)
            {
                string boneName = bones[i].Name;

                if (!string.IsNullOrEmpty(boneName))
                    boneMap.Add(boneName, i);
            }

            // Convert each animation in turn.
            Dictionary<string, AnimationClip> animationClips;
            animationClips = new Dictionary<string, AnimationClip>();

            foreach (KeyValuePair<string, AnimationContent> animation in animations)
            {
                AnimationClip processed = ProcessAnimation(animation.Value, boneMap);

                animationClips.Add(animation.Key, processed);
            }

            if (animationClips.Count == 0)
            {
                context.Logger.LogWarning(null, null, "Mesh has no animations");
                //throw new InvalidContentException(
                //            "Input file does not contain any animations.");
            }

            return animationClips;
        }

        /// <summary>
        /// Converts an intermediate format content pipeline AnimationContent
        /// object to our runtime AnimationClip format.
        /// </summary>
        static AnimationClip ProcessAnimation(AnimationContent animation,
                                              Dictionary<string, int> boneMap)
        {
            int numframes, boneIndex, frame;

            if (animation.Duration <= TimeSpan.Zero)
                throw new InvalidContentException("Animation has a zero duration.");

            // Work out how many keyframes there are
            numframes = 0;
            foreach (KeyValuePair<string, AnimationChannel> channel in animation.Channels)
            {
                // Look up which bone this channel is controlling.
                if (!boneMap.TryGetValue(channel.Key, out boneIndex))
                {
                    throw new InvalidContentException(string.Format(
                        "Found animation for bone '{0}', " +
                        "which is not part of the skeleton.", channel.Key));
                }
                numframes += channel.Value.Count;
            }
            if (numframes == 0)
                throw new InvalidContentException("Animation has no keyframes.");

            Keyframe[] keyframes = new Keyframe[numframes];

            // For each input animation channel.
            frame = 0;
            foreach (KeyValuePair<string, AnimationChannel> channel in animation.Channels)
            {
                boneMap.TryGetValue(channel.Key, out boneIndex);

                // Convert the keyframe data.
                foreach (AnimationKeyframe keyframe in channel.Value)
                {
                    keyframes[frame++] = new Keyframe(boneIndex, keyframe.Time, keyframe.Transform);
                }
            }

            // Sort the merged keyframes by time.
            Array.Sort(keyframes, delegate(Keyframe a, Keyframe b) { return a.Time.CompareTo(b.Time); });

            return new AnimationClip(animation.Duration, keyframes);
        }
    }
}
